Skip to content

S14-04 SSR-React18 SSR

[TOC]

原生实现

概述

React和Vue一样,除了支持开发SPA应用之外,其实也是支持开发SSR应用的。

在React中创建SSR应用,需要调用 ReactDOM.hydrateRoot() 函数,而不是ReactDOM.createRoot

  • createRoot :创建一个Root,接着调用其 render 函数将App直接过载到页面上
  • hydrateRoot() :创建水合Root ,是在激活的模式下渲染 App

服务端可用 ReactDOM.renderToString() 来进行渲染

image-20240911160259134

搭建Node Server

依赖包:

  • express
    • 安装:pnpm i express
  • nodemon
    • 安装:pnpm i nodemon -g
  • webpack
  • webpack-cli
  • webpack-node-externals
    • 安装:pnpm i webpack webpack-cli webpack-node-externals -D

搭建过程:

1、运行pnpm init 初始化package.json

image-20241012142213942

2、创建一个express服务器

image-20241012142227345

3、在package.json中编写npm脚本,运行node服务器

image-20241012142310169

4、打包/src/server/index.js文件

  • 打包配置/config/webpack.server.jstarget

    image-20241012142834715

  • 打包命令,--watch表示内容变化时会重新打包。

    image-20241012142934456

5、优化打包:使用 webpack-node-externals 在打包时排除node_modules中的包。

此时打包的js文件有900kb大小,需要优化。

image-20240925121711211

6、测试打包后的js文件是否可以运行(OK)

image-20241012143102061

image-20241012143144537

搭建React18 SSR Server

依赖包:

  • react
    • 安装:pnpm i react
  • react-dom
    • 安装:pnpm i react-dom
  • webpack-merge
    • 安装:pnpm i webpack-merge -D
  • babel-loader
  • @babel/preset-react
  • @babel/preset-env
    • 安装:pnpm i babel-loader @babel/preset-react @babel/preset-env -D

搭建过程:

注意: react中不用调用SSR专用的方法(如vue中的createSSRApp())生成app,可以直接返回App。其他流程和vue基本一致。

1、编写app.jsx

image-20241012144513945

2、在src/server/index.js中通过 renderToString() 方法将app实例转化为HTML字符串。并返回给前端。

image-20241012144545880

6、打包项目:pnpm run build:server

7、运行打包后的项目:pnpm run start

image-20240925125633453

8、此时页面可以展示,但是不能互动,页面中的按钮不起作用。

image-20241012144555772

Hydration

安装的依赖项同前面一样

服务器端渲染页面 + 客户端激活页面,是页面有交互效果(这个过程称为:Hydration 水合)

Hydration的具体步骤如下:

1、在src/client/index.js中通过 hydrateRoot() 创建一个App实例并挂载到#root元素上。

image-20241012151911670

2、创建配置文件webpack.client.js,并配置打包项。

image-20241012152201224

3、创建打包脚本

image-20241012152230845

4、打包项目:pnpm run build:client

image-20241012152501394

5、在src/server/index.js的HTML模板中,引入client_bundle.js。JS文件部署在静态服务器中。

注意: 在HTML模板中引入${AppHtmlString}时,起前后不能有空格或换行,否则报错

image-20241012153907298

6、运行项目:pnpm run start。此时页面中的JS代码已经激活。

image-20241012155054594

合并配置

依赖包:

  • webpack-merge:合并webpack配置。
    • 安装:pnpm i webpack-merge -D

base.config.js

image-20241012154215947

server.config.js

image-20241012154651012

client.config.js

image-20241012154648142

集成Router

依赖包:

  • react-router-dom
    • 安装:npm i react-router-dom ( 默认会自动安装 react-router )

实现过程:

1、在src/router/index.js中创建一个路由实例。

image-20241012160654308

2、在src/app.jsx中将routes转化为组件形式的路由并添加路由占位

image-20241012160851166

3、在src/server/index.js中挂载路由到app上。<App />需要用<StatciRouter>包裹

image-20241012160635766

4、在src/client/index.js中也挂载一遍路由。<App />需要用<BrowserRouter>包裹

image-20241012160517645

5、效果

image-20241012160953622

集成ReduxToolkit

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。

  • 在前面我们学习Redux的时候应该已经发现,redux的编写逻辑过于的繁琐和麻烦。

  • 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);

  • Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题;

  • 在很多地方为了称呼方便,也将之称为“RTK”;

安装

sh
npm install @reduxjs/toolkit react-redux

API

  • configureStore({ reducer, middleware, devTools, ... })返回:store,包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension。
    • 参数
    • reducer:``,Redux store 的根 reducer
    • middleware:``,要使用的中间件数组
    • devTools:``,是否启用开发工具(如 Redux DevTools),默认true
    • preloadedState:``,初始状态
    • enhancers:``,其他 store 增强器
    • 返回
    • store:``,返回的是一个 Redux store 实例,而不是一个类。因此无法创建多个 store 实例
  • createSlice({ name, initialState, reducers,... })返回:reducerSlice,用于创建一个Redux reducer和action creator的集合
    • 参数
    • nameString,用于标识这个reducer的名称,action.type会根据name生成
    • initialStateany,表示这个reducer的初始状态
    • reducers{ reducer,... },用于定义这个reducer的action creator和对应的reducer函数
      • reducer(state, action) => void,相当于之前的reducer函数
    • 返回
    • reducerSlice:``,返回一个reducer片段
  • createAsyncThunk(typePrefix, payloadCreator, options? )返回:,用于创建一个异步action creator
    • 参数
    • typePrefixString,用于标识这个异步action creator的类型前缀
    • payloadCreator(arg, thunkAPI) => Promise,用于处理异步操作并返回一个Promise对象
    • options?Object,用于配置异步action creator的一些选项
      • fulfilled:用于指定异步操作成功时的处理函数。
      • rejected:用于指定异步操作失败时的处理函数。
      • pending:用于指定异步操作进行中时的处理函数。
      • dispatchCondition:用于指定在什么条件下才会dispatch这个action的函数。
      • condition:用于指定在什么条件下才会调用payloadCreator函数的函数。
      • typeSuffixes:用于指定异步action creator的类型后缀的对象。
      • serializeError:用于指定如何序列化异步操作的错误信息的函数。

集成过程

1、创建store:configureStore()

js
  import { configureStore } from '@reduxjs/toolkit'
  import counterReducer from './features/counter'
  import homeReducer from './features/home'

+  const store = configureStore({
+    reducer: {
+      counter: counterReducer,
+      home: homeReducer
+    }
+  })

  export default store

2、创建reducer片段:createSlice()

js
  import { createSlice } from "@reduxjs/toolkit";

+  const counterSlice = createSlice({
+    name: 'counter',
+    initialState: {
+      counter: 100
+    },
+    reducers: {
+      addCounter(state, action) {
+		 state.counter += action.payload
+      },
+      subCounter(state, action) {
+        state.counter -= action.payload
+      }
+    }
+  })

+  export default counterSlice.reducer

3、在client中结合redux和react组件

image-20241012170132854

4、在server中结合redux和react组件

image-20241012170230010

5、在组件中使用 useSelector() 获取store中的数据

image-20241012170931858

6、在组件中使用 useDispatch() 修改store中的数据

image-20241012171412113

7、效果

image-20241012171429674

异步操作

1、在home.ts中发送异步请求

js
+  export const fetchHomeMultidataAction = createAsyncThunk('home/multidata', async () => {
    const res = await axios.get('http://123.207.32.32:8000/home/multidata')
    return res.data.data
  })

  const homeSlice = createSlice({
    name: 'home',
    initialState: {
      banners: [],
      recommends: []
    },
+    extraReducers(builder) {
+      builder
+        .addCase(fetchHomeMultidataAction.fulfilled, (state, { payload }) => {
          state.banners = payload.banner.list
          state.recommends = payload.recommend.list
        })
+        .addCase(fetchHomeMultidataAction.rejected, (state) => {
          console.log('fetchHomeMultidataAction.rejected')
        })
    }
  })

2、在组件中调用 fetchHomeMultidataAction()

image-20241012172750002